<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>颗粒时钟</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      canvas {
        background-color: radial-gradient(#fff, #928c8c);
        display: block;
        width: 100vw;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas"></canvas>
  </body>
  <script type="text/javascript">
    const canvas = document.getElementById('canvas')
    const ctx = canvas.getContext('2d', {
      // 将图像数据缓存在GPU内存中 ，可显著提升多次读取像素数据的速度和效率 ，用于需要频繁读取像素数据的场景
      willReadFrequently: true,
    })
    // 初始化画布
    function initCanvasSize() {
      canvas.width = window.innerWidth * devicePixelRatio
      canvas.height = window.innerHeight * devicePixelRatio
    }

    initCanvasSize()

    /* 获取max,min内的随机数 */
    function getRandom(min, max) {
      return Math.floor(Math.random() * (max + 1 - min) + min)
    }
    // 粒子类
    class Particle {
      constructor() {
        // 粒子半径
        this.size = getRandom(2 * devicePixelRatio, 7 * devicePixelRatio)
        // 粒子半径
        const r = Math.min(canvas.width, canvas.height) / 2
        // 粒子初始角度
        const rad = (getRandom(0, 360) * Math.PI) / 180
        // 圆心坐标
        const cx = canvas.width / 2
        const cy = canvas.height / 2
        // 粒子初始位置
        this.x = cx + r * Math.cos(rad)
        this.y = cy + r * Math.sin(rad)
      }
      draw() {
        ctx.beginPath()
        // 画圆(粒子)
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
        ctx.fillStyle = '#5445544d'
        // 填充颜色
        ctx.fill()
      }
      moveTo(tx, ty) {
        // 运动时间
        const duration = 500
        // 目标位置
        const sx = this.x,
          sy = this.y
        // 运动速度
        const xSpeed = (tx - sx) / duration
        const ySpeed = (ty - sy) / duration
        // 开始时间
        const startTime = Date.now()
        // 移动函数
        const _move = () => {
          const t = Date.now() - startTime
          const x = sx + xSpeed * t
          const y = sy + ySpeed * t
          this.x = x
          this.y = y
          if (t >= duration) {
            this.x = tx
            this.y = ty
            return
          }
          // x,y改动一点
          requestAnimationFrame(_move)
        }
        _move()
      }
    }
    // 粒子点数组
    const particles = []
    // 清除画布
    function clear() {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
    }
    // 绘制动画
    function draw() {
      clear()
      update()

      for (const p of particles) {
        p.draw()
      }
      requestAnimationFrame(draw)
    }
    // 定义文字
    let text = null
    draw()
    // 获取当前时间
    function getText() {
      return new Date().toTimeString().substring(0, 8)
    }
    // 更新粒子点
    function update() {
      // 当前文字内容
      const curText = getText()
      // 如果当前文字内容与上一次相同，则不更新
      if (text === curText) return
      // 更新文字内容
      text = curText
      clear()
      const { width, height } = canvas
      // 设置字体样式
      ctx.fillStyle = '#000'
      ctx.textBaseline = 'middle'
      ctx.font = `${140 * devicePixelRatio}px 'Roboto Mono', sans-serif`
      ctx.textAlign = 'center'
      ctx.fillText(text, width / 2, height / 2)
      // 获取粒子点
      const points = getPoints()
      clear()
      // 更新粒子点
      // 遍历粒子点数组，如果粒子点数组长度小于粒子点数组长度，则创建一个粒子点，反之删除一个粒子点
      for (let i = 0; i < points.length; i++) {
        const [x, y] = points[i]
        let p = particles[i]
        // 如果粒子不够，则创建一个
        if (!p) {
          p = new Particle(x, y)
          particles.push(p)
        }
        p.moveTo(x, y)
      }
      // 如果粒子太多，则删除多余的
      if (points.length < particles.length) {
        particles.splice(points.length)
      }
    }
    // 获取粒子点
    function getPoints() {
      const points = []
      // 获取画布上的文字像素点
      const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height)
      const gap = 6
      for (let i = 0; i < canvas.width; i += gap) {
        for (let j = 0; j < canvas.height; j += gap) {
          const index = (i + j * canvas.width) * 4
          const r = data[index]
          const g = data[index + 1]
          const b = data[index + 2]
          const a = data[index + 3]
          // 如果像素点为黑色，则添加到粒子点数组中
          if (r === 0 && g === 0 && b === 0 && a === 255) {
            points.push([i, j])
          }
        }
      }
      return points
    }
  </script>
</html>
